package com.github.netty.webapp;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import javax.servlet.ServletContainerInitializer;
import javax.servlet.annotation.HandlesTypes;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.ServletSecurity;
import javax.servlet.annotation.WebFilter;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebListener;
import javax.servlet.annotation.WebServlet;

import org.apache.bcel.Const;
import org.apache.bcel.classfile.AnnotationEntry;
import org.apache.bcel.classfile.ClassParser;
import org.apache.bcel.classfile.JavaClass;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterInfoDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import org.apache.tomcat.util.descriptor.web.ServletDef;
import org.apache.tomcat.util.descriptor.web.ServletInfoDef;
import org.apache.tomcat.util.descriptor.web.ServletMappingDef;

/**
 * @author 三刀（zhengjunweimail@163.com）
 * @version V1.0 , 2021/6/27
 */
public class WebAnnotationsLoader {
	private final Map<ServletContainerInitializer, Set<Class<?>>> initializerClassMap = new LinkedHashMap<>();
	/**
	 * Map of Types to ServletContainerInitializer that are interested in those
	 * types.
	 */
	private final Map<Class<? extends Annotation>, Set<ServletContainerInitializer>> typeInitializerMap = new HashMap<>();
	private final ClassLoader classLoader;
	/**
	 * Flag that indicates if at least one {@link HandlesTypes} entry is present
	 * that represents an annotation.
	 */
	private boolean handlesTypesAnnotations = false;
	/**
	 * Flag that indicates if at least one {@link HandlesTypes} entry is present
	 * that represents a non-annotation.
	 */
	private boolean handlesTypesNonAnnotations = false;

	private Map<Class<? extends Annotation>, List<String>> annotations = new HashMap<>();//放 WebListener 的

	private final Map<String, ServletInfoDef> servlets = new HashMap<>();
	
	private final Map<String,FilterInfoDef> filters = new HashMap<>();

	public WebAnnotationsLoader(ClassLoader classLoader) {
		this.classLoader = classLoader;
	}

	public void add(ServletContainerInitializer initializer, Class<? extends Annotation> handlesType) {
		typeInitializerMap.computeIfAbsent(handlesType, k -> new HashSet<>()).add(initializer);
		if (handlesType.isAnnotation()) {
			handlesTypesAnnotations = true;
		} else {
			handlesTypesNonAnnotations = true;
		}
	}

	public void clear() {
		executorService.shutdownNow();
		executorService = null;
		
//		initializerClassMap.clear();
//		typeInitializerMap.clear();
//		annotations.clear();
//		servlets.clear();
//		filters.clear();
	}

	private ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

//	public void scanAnnotations() {
//		scanAnnotations(null);
//	}

	public void scanAnnotations(final String basePackage) {
		if (!(classLoader instanceof URLClassLoader)) {
			return;
		}
		if (basePackage == null || basePackage.length() < 1) {
			return;
		}
		Map<String, JavaClassCacheEntry> javaClassCache = new HashMap<>();
		URL[] urls = null;
		String path = basePackage.replace('.', '/');
		try {
			Enumeration<URL> resources = classLoader.getResources(path);
			List<URL> list = new ArrayList<>();
			while (resources.hasMoreElements()) {
				URL resource = resources.nextElement();
				list.add(resource);
			}
			urls = list.toArray(new URL[list.size()]);
			resources = null;
		} catch (IOException e) {
			e.printStackTrace();
		}

		CountDownLatch count = new CountDownLatch(urls.length);
		for (URL url : urls) {
			executorService.execute(() -> {
//                long start = System.currentTimeMillis();
				try {
					if ("jar".equals(url.getProtocol()) || url.toString().endsWith(".jar")) {
						processAnnotationsJar(url, javaClassCache);
					} else if ("file".equals(url.getProtocol())) {
						try {
							processAnnotationsFile(new File(url.toURI()), javaClassCache);
						} catch (URISyntaxException e) {
							e.printStackTrace();
						}
					}
				} finally {
//                    System.out.println("scan cost: " + (System.currentTimeMillis() - start));
					count.countDown();
				}
			});

		}
		try {
			count.await();
		} catch (InterruptedException e) {
			throw new RuntimeException(e);
		}
		javaClassCache.clear();
	}

	public Map<ServletContainerInitializer, Set<Class<?>>> getInitializerClassMap() {
		return initializerClassMap;
	}

	public Map<String, ServletInfoDef> getServlets() {
		return servlets;
	}

	public Map<String, FilterInfoDef> getFilters() {
		return filters;
	}

	public List<String> getAnnotations(Class<? extends Annotation> clazz) {
		List<String> classes = annotations.get(clazz);
		if (classes == null || classes.size() < 1) {
			classes = new ArrayList<>();
		}
		return classes;
	}

	private void processAnnotationsJar(URL url, Map<String, JavaClassCacheEntry> javaClassCache) {
		try (JarFile jarFile = new JarFile(url.getFile());) {
			Enumeration<JarEntry> entrys = jarFile.entries();

			while (entrys.hasMoreElements()) {
				JarEntry jarEntry = entrys.nextElement();
				String entryName = jarEntry.getName();
				if (entryName.endsWith(".class")) {
					try {
						ClassParser parser = new ClassParser(jarFile.getInputStream(jarEntry));
						JavaClass clazz = parser.parse();
						checkAnnotation(clazz, javaClassCache);
					} catch (IOException e) {
						e.printStackTrace();
					}
				}

			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	private void processAnnotationsFile(File file, Map<String, JavaClassCacheEntry> javaClassCache) {

		if (file.isDirectory()) {
			// Returns null if directory is not readable
			String[] dirs = file.list();
			if (dirs != null) {
				for (String dir : dirs) {
					processAnnotationsFile(new File(file, dir), javaClassCache);
				}
			}
		} else if (file.getName().endsWith(".class") && file.canRead()) {
			try (FileInputStream fis = new FileInputStream(file)) {
				ClassParser parser = new ClassParser(fis);
				JavaClass clazz = parser.parse();
				checkAnnotation(clazz, javaClassCache);
			} catch (Exception e) {
				e.printStackTrace();
			}
		} else {
			System.out.println("skip file:" + file.getAbsolutePath());
		}
	}

	/**
	 * 检查是否包含待加载的注解
	 */
	private void checkAnnotation(JavaClass javaClass, Map<String, JavaClassCacheEntry> javaClassCache)
			throws ClassNotFoundException {
		if ((javaClass.getAccessFlags() & Const.ACC_ANNOTATION) != 0) {
			// Skip annotations.
			return;
		}

		String className = javaClass.getClassName();

		if (javaClass.getAnnotationEntries() != null) {
			for (AnnotationEntry entry : javaClass.getAnnotationEntries()) {
				System.out.println(entry.getAnnotationType() + " " + entry.getElementValuePairs());
				String annotationName = getClassName(entry.getAnnotationType());
				if (WebListener.class.getName().equals(annotationName)) {
					annotations.computeIfAbsent(WebListener.class, aClass -> new ArrayList<>()).add(className);
				} else if (WebServlet.class.getName().equals(annotationName)) {
					Class<?> clazz = classLoader.loadClass(className);
					WebServlet webServlet = clazz.getAnnotation(WebServlet.class);
					String name = webServlet.name();
					if (name.isEmpty()) {
						name = className;
					}
					ServletDef servletInfo = new ServletDef();
					servletInfo.setServletName(name);
					servletInfo.setLoadOnStartup(String.valueOf(webServlet.loadOnStartup()));
					servletInfo.setServletClass(className);
					servletInfo.setAsyncSupported(String.valueOf(webServlet.asyncSupported()));
					servletInfo.setDescription(webServlet.description());
					servletInfo.setSmallIcon(webServlet.smallIcon());
					servletInfo.setLargeIcon(webServlet.largeIcon());
					for (WebInitParam param : webServlet.initParams()) {
						servletInfo.addInitParameter(param.name(), param.value());
					}
					
					ServletMappingDef servletMapping = new ServletMappingDef();
					Set<String> urlPatterns = new HashSet<>();
					for (String urlPattern : webServlet.urlPatterns()) {
						urlPatterns.add(urlPattern);
					}
					for (String url : webServlet.value()) {
						urlPatterns.add(url);
					}
					servletMapping.setServletName(name);
					servletMapping.setUrlPatterns(urlPatterns);
					servlets.put(name, new ServletInfoDef(servletInfo, servletMapping));
				}else if(WebFilter.class.getName().equals(annotationName)) {
					Class<?> clazz = classLoader.loadClass(className);
					WebFilter webFilter = clazz.getAnnotation(WebFilter.class);
					String name = webFilter.filterName();
					if (name.isEmpty()) {
						name = className;
					}
					FilterDef filterDef = new FilterDef();
					filterDef.setFilterName(name);
					filterDef.setAsyncSupported(String.valueOf(webFilter.asyncSupported()));
					filterDef.setDescription(webFilter.description());
					filterDef.setDisplayName(webFilter.displayName());
					filterDef.setFilterClass(className);
					filterDef.setLargeIcon(webFilter.largeIcon());
					filterDef.setSmallIcon(webFilter.smallIcon());
					for (WebInitParam param : webFilter.initParams()) {
						filterDef.addInitParameter(param.name(), param.value());
					}
					FilterMap filterMap = new FilterMap();
					filterMap.setFilterName(name);
					filterMap.setDispatcher(webFilter.dispatcherTypes()[0].name());
					for (String urlPattern : webFilter.urlPatterns()) {
						filterMap.addURLPattern(urlPattern);
					}
					for(String servletName : webFilter.servletNames()) {
						filterMap.addServletName(servletName);
					}
					filters.put(name, new FilterInfoDef(filterDef, filterMap));
				}else if(ServletSecurity.class.getName().equals(annotationName)) {
					System.out.println("Sorry,ServletSecurity is not support!");
				}else if(MultipartConfig.class.getName().equals(annotationName)) {
					System.out.println("Sorry,MultipartConfig is not support!");
				}
			}
		}

		Class<?> clazz = null;
		if (handlesTypesNonAnnotations) {
			// 从 classPath 扫描并加载 class
			populateJavaClassCache(className, javaClass, javaClassCache);
			JavaClassCacheEntry entry = javaClassCache.get(className);
			if (entry == null) {
				return;
			}
			if (entry.getSciSet() == null) {
				populateSCIsForCacheEntry(entry, javaClassCache);
			}
			// 当前 class 为 HandlesType 标注的接口
			if (!entry.getSciSet().isEmpty()) {
				// Need to try and load the class
				try {
					clazz = classLoader.loadClass(className);
					for (ServletContainerInitializer sci : entry.getSciSet()) {
						Set<Class<?>> classes = initializerClassMap.computeIfAbsent(sci, k -> new HashSet<>());
						classes.add(clazz);
					}
				} catch (ClassNotFoundException e) {
					e.printStackTrace();
					return;
				}
			}
		}

		AnnotationEntry[] annotationEntries = javaClass.getAnnotationEntries();
		if (handlesTypesAnnotations && annotationEntries != null) {
			for (Map.Entry<Class<? extends Annotation>, Set<ServletContainerInitializer>> entry : typeInitializerMap
					.entrySet()) {
				// 当前类非注解
				if (!entry.getKey().isAnnotation()) {
					continue;
				}
				String entryClassName = entry.getKey().getName();
				for (AnnotationEntry annotationEntry : annotationEntries) {
					if (!entryClassName.equals(getClassName(annotationEntry.getAnnotationType()))) {
						continue;
					}
					if (clazz == null) {
						try {
							clazz = classLoader.loadClass(className);
						} catch (ClassNotFoundException e) {
							e.printStackTrace();
							return;

						}
					}
					for (ServletContainerInitializer sci : entry.getValue()) {
						initializerClassMap.get(sci).add(clazz);
					}
					break;
				}
			}
		}
	}

	private void populateSCIsForCacheEntry(JavaClassCacheEntry cacheEntry,
			Map<String, JavaClassCacheEntry> javaClassCache) {
		Set<ServletContainerInitializer> result = new HashSet<>();

		// Super class
		String superClassName = cacheEntry.getSuperclassName();
		JavaClassCacheEntry superClassCacheEntry = javaClassCache.get(superClassName);

		// Avoid an infinite loop with java.lang.Object
		if (cacheEntry.equals(superClassCacheEntry)) {
			cacheEntry.setSciSet(Collections.emptySet());
			return;
		}

		// May be null of the class is not present or could not be loaded.
		if (superClassCacheEntry != null) {
			if (superClassCacheEntry.getSciSet() == null) {
				populateSCIsForCacheEntry(superClassCacheEntry, javaClassCache);
			}
			result.addAll(superClassCacheEntry.getSciSet());
		}
		result.addAll(getSCIsForClass(superClassName));

		// Interfaces
		for (String interfaceName : cacheEntry.getInterfaceNames()) {
			JavaClassCacheEntry interfaceEntry = javaClassCache.get(interfaceName);
			// A null could mean that the class not present in application or
			// that there is nothing of interest. Either way, nothing to do here
			// so move along
			if (interfaceEntry != null) {
				if (interfaceEntry.getSciSet() == null) {
					populateSCIsForCacheEntry(interfaceEntry, javaClassCache);
				}
				result.addAll(interfaceEntry.getSciSet());
			}
			result.addAll(getSCIsForClass(interfaceName));
		}

		cacheEntry.setSciSet(result.isEmpty() ? Collections.emptySet() : result);
	}

	private Set<ServletContainerInitializer> getSCIsForClass(String className) {
		for (Map.Entry<Class<? extends Annotation>, Set<ServletContainerInitializer>> entry : typeInitializerMap
				.entrySet()) {
			Class<?> clazz = entry.getKey();
			if (!clazz.isAnnotation()) {
				if (clazz.getName().equals(className)) {
					return entry.getValue();
				}
			}
		}
		return Collections.emptySet();
	}

	private void populateJavaClassCache(String className, JavaClass javaClass,
			Map<String, JavaClassCacheEntry> javaClassCache) {
		if (javaClassCache.containsKey(className) || className.startsWith("java") || className.startsWith("sun.")) {
			return;
		}

		// 缓存当前加载的类
		javaClassCache.put(className, new JavaClassCacheEntry(javaClass));

		// 加载父类
		populateJavaClassCache(javaClass.getSuperclassName(), javaClassCache);

		// 加载接口
		for (String interfaceName : javaClass.getInterfaceNames()) {
			populateJavaClassCache(interfaceName, javaClassCache);
		}
	}

	/**
	 * 加载并缓存指定类，如果该类有父类或实现了某些接口，则递归加载
	 *
	 * @param className
	 * @param javaClassCache
	 */
	private void populateJavaClassCache(String className, Map<String, JavaClassCacheEntry> javaClassCache) {
		if (javaClassCache.containsKey(className)) {
			return;
		}
		String name = className.replace('.', '/') + ".class";
		try (InputStream is = classLoader.getResourceAsStream(name)) {
			if (is == null) {
				return;
			}
			ClassParser parser = new ClassParser(is);
			JavaClass clazz = parser.parse();
			populateJavaClassCache(clazz.getClassName(), clazz, javaClassCache);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	private String getClassName(String internalForm) {
		if (!internalForm.startsWith("L")) {
			return internalForm;
		}

		// Assume starts with L, ends with ; and uses / rather than .
		return internalForm.substring(1, internalForm.length() - 1).replace('/', '.');
	}

}